package bancosys.tec.services.implementor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import jmine.tec.di.injector.DependencyInjector;
import jmine.tec.di.injector.Injector;
import jmine.tec.di.metadata.DependencyMetaData;
import jmine.tec.di.scope.TypedScope;
import jmine.tec.di.type.TypeConvertionException;
import jmine.tec.di.validation.StaticValidator;
import jmine.tec.services.annotations.ServiceImplementor;
import jmine.tec.services.api.ActionsEnum;
import jmine.tec.services.api.ServiceExecutionException;

import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.SpringTransactionAnnotationParser;

import bancosys.tec.services.api.AbstractService;
import bancosys.tec.services.implementor.descriptor.AbstractServiceAnnotationParser;
import bancosys.tec.services.implementor.descriptor.InstanceCreator;
import bancosys.tec.services.implementor.descriptor.LazySingletonInstanceCreator;
import bancosys.tec.services.implementor.descriptor.ServiceExecutionProxy;
import bancosys.tec.services.implementor.descriptor.ServiceOutputExtractor;
import bancosys.tec.services.implementor.descriptor.ServiceValidator;

/**
 * Implementa {@link ServiceImplementorAccessor}. Serve como fachada para a criacao e execucao de ServiceImplementors. É imutável após a
 * construção.
 * 
 * @author MarceloT
 */
public class UnmodifiableServiceImplementorAccessor implements ServiceImplementorAccessor {

    /**
     * {@link Comparator} para {@link DependencyMetaData}
     * 
     * @author takeshi
     */
    public static final class DependencyMetaDataComparator implements Comparator<DependencyMetaData> {

        /**
         * {@inheritDoc}
         */
        public int compare(DependencyMetaData o1, DependencyMetaData o2) {
            return o1.getPropertyName().compareTo(o2.getPropertyName());
        }
    }

    private final ServiceImplementor annotation;

    private final Class<?> implementorType;

    private final DependencyInjector injector;

    private final ServiceExecutionProxy proxy;

    private final ServiceOutputExtractor extractor;

    private final ServiceValidator validator;

    private final InstanceCreator creator;

    /**
     * Cria um novo {@link UnmodifiableServiceImplementorAccessor}
     * 
     * @param implementorType o tipo concreto
     * @param injector o {@link DependencyInjector}
     * @param proxy o {@link ServiceExecutionProxy}
     * @param extractor o {@link ServiceOutputExtractor}
     * @param validator o {@link ServiceValidator}
     * @param creator o {@link InstanceCreator}
     */
    public UnmodifiableServiceImplementorAccessor(Class<?> implementorType, DependencyInjector injector, ServiceExecutionProxy proxy,
            ServiceOutputExtractor extractor, ServiceValidator validator, InstanceCreator creator) {
        super();
        final Class<?> annotated = AbstractServiceAnnotationParser.findTypeWithAnnotation(implementorType);
        if (annotated == null) {
            throw new IllegalArgumentException("No annotation @ServiceImplementor found on class: " + implementorType);
        }
        this.annotation = annotated.getAnnotation(ServiceImplementor.class);
        this.implementorType = implementorType;
        this.injector = injector;
        this.proxy = proxy;
        this.extractor = extractor;
        this.validator = validator;
        if (this.isStateless()) {
            this.verifyStateless(creator, injector);
            this.creator = new LazySingletonInstanceCreator(creator);
        } else {
            this.creator = creator;
        }
    }

    /**
     * @param creator2 the instance creator
     * @param injector2 the dependency injector
     */
    private void verifyStateless(InstanceCreator creator2, DependencyInjector injector2) {
        List<DependencyMetaData> dependencies = creator2.getDependencies();
        this.allResolvable(dependencies);
        Injector[] allInjectors = injector2.getAllInjectors();
        List<DependencyMetaData> deps = new ArrayList<DependencyMetaData>(allInjectors.length);
        for (Injector inj : allInjectors) {
            deps.add(inj.getDependencyMetaData());
        }
        this.allResolvable(deps);
    }

    /**
     * @param dependencies the dependencies
     */
    private void allResolvable(List<DependencyMetaData> dependencies) {
        for (DependencyMetaData dependencyMetaData : dependencies) {
            if (!dependencyMetaData.getResolver().isStatic()) {
                throw new IllegalArgumentException("servico " + this.getServiceName()
                        + " marcado como stateless, mas apresenta dependencia para: " + dependencyMetaData.getPropertyName());
            }
        }
    }

    /**
     * @return {@link ActionsEnum}
     */
    public ActionsEnum getActionType() {
        return this.annotation.action();
    }

    /**
     * @return {@link String} o nome do servico
     */
    public String getServiceName() {
        if (this.annotation.serviceName().length() == 0) {
            return AbstractService.getServiceName(this.implementorType);
        } else {
            return this.annotation.serviceName();
        }
    }

    /**
     * @return boolean
     */
    public boolean isStateless() {
        return this.annotation.stateless();
    }

    /**
     * Cria uma nova instancia e injeta as dependencias se possivel.
     * 
     * @param scope o escopo
     * @return Object
     * @throws TypeConvertionException e
     */
    public Object createWithDependencies(TypedScope scope) throws TypeConvertionException {
        final Object target = this.creator.hydrateAndCreateInstance(scope);
        if (target != null) {
            this.injector.inject(target, scope);
        }
        return target;
    }

    /**
     * @param service o servico
     * @param scope o escopo
     * @throws Exception a excecao lancada durante a execucao do servico.
     */
    public void executeAndFillScope(Object service, TypedScope scope) throws Exception {
        Object ret = this.proxy.hydrateAndExecute(service, scope);
        String outName = this.proxy.getOutputName(scope);
        Map<String, Object> output = this.extractor.getOutput(service, scope);
        if (outName != null) {
            output.put(outName, ret);
        }
        if (!output.isEmpty()) {
            Set<Entry<String, Object>> set = output.entrySet();
            for (Entry<String, Object> entry : set) {
                String normKey = entry.getKey().startsWith("$") ? entry.getKey() : "$" + entry.getKey();
                scope.putDefinition(normKey, entry.getValue());
            }
        }
    }

    private DependencyMetaData[] allMetadata;

    /**
     * {@inheritDoc}
     */
    public List<String> validateStaticState(TypedScope scope) {
        if (this.allMetadata == null) {
            final List<DependencyMetaData> deps = this.getAllDependencies();
            this.allMetadata = deps.toArray(new DependencyMetaData[deps.size()]);
            Arrays.sort(this.allMetadata, new DependencyMetaDataComparator());
        }
        return StaticValidator.validateSimple(scope, this.allMetadata);
    }

    /**
     * {@inheritDoc}
     */
    public void validateBeforeExecution(Object target, TypedScope scope) throws ServiceExecutionException {
        this.validator.validateThrowException(target, scope);
    }

    /**
     * @return {@link Class}
     */
    public Class<?> getImplementorType() {
        return this.implementorType;
    }

    /**
     * @return {@link List} de {@link DependencyMetaData}
     */
    public List<DependencyMetaData> getAllDependencies() {
        List<DependencyMetaData> dependencies = this.creator.getDependencies();
        Injector[] injectors = this.injector.getAllInjectors();
        List<DependencyMetaData> list = new ArrayList<DependencyMetaData>(dependencies.size() + injectors.length);
        for (DependencyMetaData dependencyMetaData : dependencies) {
            list.add(dependencyMetaData);
        }
        for (Injector inj : injectors) {
            list.add(inj.getDependencyMetaData());
        }
        DependencyMetaData[] executionDependencies = this.proxy.getExecutionDependencies();
        for (DependencyMetaData dependencyMetaData : executionDependencies) {
            list.add(dependencyMetaData);
        }
        return list;
    }

    /**
     * {@inheritDoc}
     */
    public ServiceOutputExtractor getOutputExtractor() {
        return this.extractor;
    }

    /**
     * {@inheritDoc}
     */
    public TransactionDefinition getTransactionDefinition() {
        return new SpringTransactionAnnotationParser().parseTransactionAnnotation(this.implementorType);
    }

    /**
     * @return {@link DependencyInjector}
     */
    public DependencyInjector getInjector() {
        return this.injector;
    }

    /**
     * @return {@link ServiceExecutionProxy}
     */
    public ServiceExecutionProxy getServiceExecutionProxy() {
        return this.proxy;
    }

    /**
     * @return {@link ServiceOutputExtractor}
     */
    public ServiceOutputExtractor getExtractor() {
        return this.extractor;
    }

    /**
     * @return {@link ServiceValidator}
     */
    public ServiceValidator getValidator() {
        return this.validator;
    }

    /**
     * @return {@link InstanceCreator}
     */
    public InstanceCreator getCreator() {
        return this.creator;
    }

}
